-- Copyright (c) 2025 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local skynet = require 'skynet'
local bs = require 'mc.bitstring'
local log = require 'mc.logging'
local vos = require 'utils.vos'
local definition = require 'canbus.definition'
local canbus_collection = require 'canbus.collection'
local canbus_dispatcher = require 'canbus.dispatcher'

local heartbeat = {}

heartbeat.payload_pattern = bs.new([[<<cmd, identifier, channel>>]])

heartbeat.collections = {}

function heartbeat.register()
    local canbus_enabled = canbus_collection.get_instance().canbus_enabled
    if not canbus_enabled then
        log:notice('canbus is disabled and cannot register heartbeat')
        return
    end

    -- register callbacks to dispatcher
    canbus_dispatcher.get_instance():register_cmd_handler(definition.can_cmds.HEARTBEAT_REQ, heartbeat.recv)

    -- start heartbeat sending task
    skynet.fork_once(function ()
        while true do
            heartbeat.send()
            skynet.sleep(definition.heartbeat_interval.SEND // 10)
        end
    end)

    -- start heartbeat monitor task
    skynet.fork_once(function ()
        while true do
            heartbeat.monitor()
            skynet.sleep(definition.heartbeat_interval.MONITOR // 10)
        end
    end)
end

function heartbeat.send()
    -- heartbeat will be transmitted on each channels using boardcast.
    local collections = canbus_collection.get_instance().collections
    for k, v in pairs(collections) do
        local payload = heartbeat.payload_pattern:pack({
            cmd = definition.can_cmds.HEARTBEAT_REQ,
            identifier = v.identifier,
            channel = k
        })
        if not payload then
            log:warn('canbus [%s] heartbeat payload generate failed.', k)
            goto continue
        end

        canbus_collection.get_instance():send(payload, k, definition.dest.BOARDCAST)
        ::continue::
    end
end

local function validate_message(can_id, msgs)
    -- ignore the self heartbeat
    local identifier = canbus_collection.get_instance().identifier
    if can_id.src_id == identifier then
        log:info('heartbeat is self [id: %s] and ignore it', identifier)
        return false
    end

    if not msgs or type(msgs) ~= 'table' or not next(msgs) then
        log:error('heartbeat message format is invalid')
        return false
    end

    local msg = #msgs == 1 and msgs[1]
    if not msg then
        log:error('heartbeat message is multiple frames')
        return false
    end

    return true
end

-- process heartbeat message
function heartbeat.recv(can_id, channel, msgs)
    log:debug('canbus heartbeat is received from channel [%s]', channel)

    -- validate heartbeat message is valid or not
    if not validate_message(can_id, msgs) then
        return
    end

    -- record the heartbeat of remote BMCs
    heartbeat.collections[can_id.src_id] = {tick = vos.vos_tick_get(), times = 0}
end

function heartbeat.monitor()
    local cur_tick = vos.vos_tick_get()
    for k, v in pairs(heartbeat.collections) do
        if not v.tick then
            v.tick = vos.vos_tick_get()
            goto continue
        end
        if not v.times then
            v.times = 0
        end
        if cur_tick - v.tick > definition.heartbeat_interval.MONITOR then
            v.times = v.time + 1
            log:notice('BMC [%s] heartbeat is timeout for [%d] times')
        else
            goto continue
        end

        -- generate a running log while the heartbeat lost reaches running threshold
        if not v.is_running and v.times >= definition.heartbeat_threshold.RUNNING then
            log:running(log.RLOG_INFO, 'BMC [%s] heartbeat is lost')
            v.is_running = true
        end

        -- generate a maintenance log while the heartbeat lost reaches maintenance threshold
        if not v.is_maintenance and v.times >= definition.heartbeat_threshold.MAINTENANCE then
            -- NOTE: to be fixed by specified business strategy
            v.is_maintenance = true
        end

        -- generate a alarm while the heartbeat lost reaches alarm threshold
        if not v.alarmed and v.times >= definition.heartbeat_threshold.ALARM then
            -- NOTE: to be fixed by specified business strategy
            v.alarmed = true
        end
        ::continue::
    end
end

return heartbeat